<html>
<head>
  <title>0.面向对象设计原则</title>
  <basefont face="微软雅黑" size="2" />
  <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
  <meta name="exporter-version" content="YXBJ Windows/602628 (zh-CN, DDL); Windows/10.0.0 (Win64); EDAMVersion=V2;"/>
  <style>
    body, td {
      font-family: 微软雅黑;
      font-size: 10pt;
    }
  </style>
</head>
<body>
<a name="26971"/>
<h1>0.面向对象设计原则</h1>
<div>
<table bgcolor="#D4DDE5" border="0">
<tr><td><b>创建时间：</b></td><td><i>2020/10/24 12:58</i></td></tr>
<tr><td><b>更新时间：</b></td><td><i>2021/1/27 6:07</i></td></tr>
<tr><td><b>作者：</b></td><td><i>YuGe</i></td></tr>
</table>
</div>
<br/>

<div>
<span><div><div><span style="font-size: 12pt; font-weight: bold;">〇、概述</span></div><ul><li><div><span style="font-size: 16px;">主要针对设计软件的可维护性和可复用性</span></div></li><li><div><span style="font-size: 16px;">所有的设计模式都有一个统一的思路：封装变化的部分！</span></div><div><span style="font-size: 12pt;">分析变化的部分是什么，如何封装。</span></div></li><li><div><span style="font-size: 12pt;">软件设计最大的难题就是应对需求的变化，但是纷繁复杂的需求变化又是不可预料的。我们要为不可预料的事情做好准备，这本身就是一件非常痛苦的事情，但是大师们还是给我们提出了非常好的6大设计原则以及23个设计模式来“封装”未来的变化。</span></div></li><li><div><span style="font-size: 16px;">最常用的是6种面向对象设计原则：</span></div></li><li><div><span style="font-size: 16px;">单一职责原则（Single Responsibility Principle, SRP）：一个类只负责一个功能领域中的相应职责。</span></div></li><li><div><span style="font-size: 16px;">开闭原则（Open-Closed</span> <span style="font-size: 16px;">Principle, OCP</span><span style="font-size: 16px;">）：</span><span style="font-size: 12pt;">软件实体应对扩展开放， 而对修改关闭。</span></div></li><li><div><span style="font-size: 16px;">里氏代换原则（Liskov Substitution</span> <span style="font-size: 16px;">Principle, LSP</span><span style="font-size: 16px;">）：</span><span style="font-size: 12pt;">所有引用基类对象的地方能够透明地使用</span><span style="font-size: 12pt; color: unset; font-family: unset;">其子类的对象</span><span style="font-size: 12pt; color: unset; font-family: unset;">。</span></div></li><li><div><span style="font-size: 16px;">接口隔离原则（Interface Segregation</span> <span style="font-size: 16px;">Principle, ISP</span><span style="font-size: 16px;">）：</span><span style="font-size: 12pt;">使用多个专门的接口，而不使用单一的总</span><span style="font-size: 12pt; color: unset; font-family: unset;">接口。</span></div></li><li><div><span style="font-size: 16px;">依赖倒转原则（Dependence Inversion</span> <span style="font-size: 16px;">Principle, DIP</span><span style="font-size: 16px;">）：抽象不应该依赖于细节，细节应该依赖于抽象。</span></div></li><li><div><span style="font-size: 16px;">迪米特法则（Law of Demeter, LoD）：一个软件实体应当尽可能少的与其他实体发生相互作用。</span></div></li></ul><div style="margin-left: 40px;"><span style="font-size: 12pt;">把前6个原则的首字母（里氏替换原则和迪米特法则的首字母重复，只取一个）联合起来就是SOLID（solid，稳定的），其代表的含义也就是把这6个原则结合使用的好处：建立稳定、灵活、健壮的设计。</span></div><ul><li><div><span style="font-size: 16px;">面向对象设计的四个好处：可维护、可扩展、可复用、灵活性好。</span></div></li><li><div><span style="font-size: 12pt;">需要注意，设计是有限度的，不能无限地考虑未来的变更情况，否则就会陷入设计的泥潭中而不能自拔。</span></div><div><span style="font-size: 16px;">没有任何原则是法律，所有的原则都应该在有帮助的时候才遵守，所有的设计都不免需要折衷。虽然原则提供了方针，但在采用原则之前，必须全盘考虑所有的因素。</span></div></li><li><div><span style="font-size: 16px;">此外，还有</span><span style="font-size: 16px; font-weight: bold;">合成复用原则</span><span style="font-size: 16px;">、</span><span style="font-size: 16px; font-weight: bold;">松耦合原则</span><span style="font-size: 16px;">等。</span></div></li></ul><div><span style="font-size: 12pt; font-weight: bold;">一、单一职责原则</span></div><ul><li><div><span style="font-size: 16px;">定义</span></div></li><ul><li><div><span style="font-size: 16px;">就一个类而言，应该仅有一个引起它变化的原因。</span></div></li><li><div><span style="font-size: 16px;">一个类只负责一个功能领域中的相应职责。</span></div></li></ul><li><div><span style="font-size: 12pt;">单一职责原则是实现高内聚、低耦合的指导方针，它是最简单但又最难运用的原则</span><span style="font-size: 16px;">，用于控制类的粒度大小。</span></div></li><li><div><span style="font-size: 12pt;">在编程时，很自然会给一个类增加各种各样的功能，这其实是很糟糕的，维护麻烦，复用不可能，也缺乏灵活性。</span></div></li><li><div><span style="font-size: 16px;">如果一个类承担的职责过多，就等于把这些职责耦合在一起，一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计。当变化发生时，设计会遭受到意想不到的破坏。</span></div></li><li><div><span style="font-size: 16px;">软件设计真正要做的许多内容，就是发现职责并把那些职责相互分离。</span></div></li><li><div><span style="font-size: 16px;">判断职责是否应当分离：如果你能够想到多于一个的动机去改变一个类，那么这个类就具有多于一个类的职责。</span></div></li><li><div><span style="font-size: 16px;">编程时，要在类的职责分离上多思考，做到单一职责，这样代码才是真正的易维护、易扩展、易复用。</span></div></li><li><div><span style="font-size: 12pt;">单一职责原则有什么好处：</span></div></li><ul><li><div><span style="font-size: 12pt;">类的复杂性降低，实现什么职责都有清晰明确的定义；</span></div></li><li><div><span style="font-size: 12pt;">可读性提高；</span></div></li><li><div><span style="font-size: 12pt;">可维护性提高；</span></div></li></ul><li><div><span style="font-size: 12pt;">变更引起的风险降低。变更是必不可少的，如果接口的单一职责做得好，一个接口修改只对相应的实现类有影响，对其他的接口无影响，这对系统的扩展性、维护性都有非常大的帮助。</span></div></li><li><div><span style="font-size: 12pt;">单一职责原则很难在项目中得到体现：</span></div></li><ul><li><div><span style="font-size: 12pt;">单一职责原则最难划分的就是职责。一个职责一个接口，但问题是“职责”没有一个量化的标准。</span></div></li><li><div><span style="font-size: 12pt;">单一职责原则提出了一个编写程序的标准，用“职责”或“变化原因”来衡量接口或类设计得是否优良，但是“职责”和“变化原因”都是不可度量的，因项目而异，因环境而异。</span></div></li><li><div><span style="font-size: 12pt;">对于接口，我们在设计的时候一定要做到单一，但是对于实现类就需要多方面考虑了。生搬硬套单一职责原则会引起类的剧增，给维护带来非常多的麻烦，而且过分细分类的职责也会人为地增加系统的复杂性。</span></div></li></ul><li><div><span style="font-size: 12pt;">单一职责适用于接口、类，同时也适用于方法，一个方法尽可能做一件事情，方法职责要清晰、单一，不要让别人猜测这个方法可能是用来处理什么逻辑的。</span></div></li><li><div><span style="font-size: 12pt;">类的单一职责确实受非常多因素的制约，纯理论地来讲，这个原则是非常优秀的，但是现实有现实的难处，你必须去考虑项目工期、成本、人员技术水平、硬件情况、网络情况甚至有时候还要考虑政府政策、垄断协议等因素。对于单一职责原则，接口一定要做到单一职责，类的设计尽量做到只有一个原因引起变化。</span></div></li><li><div><span style="font-size: 12pt;">单一职责原则告诉我们：一个类不能太“累”！在软件系统中，一个类（大到模块，小到方法）承担的职责越多，它被复用的可能性就越小，而且一个类承担的职责过多，就相当于将这些职责耦合在一起，当其中一个职责变化时，可能会影响其他职责的运作，因此要将这些职责进行分离，将不同的职责封装在不同的类中，即将不同的变化原因封装在不同的类中，如果多个职责总是同时发生改变则可将它们封装在同一类中。</span></div></li><li><div><span style="font-size: 16px;">我们知道要避免类内的改变，因为修改代码很容易造成许多潜在的错误。如果有一个类具有两个改变的原因，那么这会使得将来该类的变化几率上升，而当它真的改变时，设计中同时有两个方面将会受到影响。</span></div></li><li><div><span style="font-size: 16px;">单一 职责原则听起来很容易，但做起来并不简单：区分设计中的责任，是最困难的事情之一。</span></div><div><span style="font-size: 16px;">我们的大脑很习惯看着一大群的行为，然后将它们集中在一起，尽管它们可能属于两个或者多个不同的责任。想要成功的唯一方法，就是努力不懈的检查你的设计，随着系统成长，随时观察有没有迹象显示某个类改变的原因超出一个。</span></div></li><li><div><span style="font-size: 16px;">内聚（cohension）用来度量一个类或模块紧密的达到单一目的或责任。</span></div><div><span style="font-size: 16px;">当一个模块或一个类被设计成只支持一组相关的功能时，就说它具有高内聚，反之，当被设计成支持一组不相关的功能时，就说它具有低内聚。</span></div><div><span style="font-size: 16px;">内聚是一个比单一职责原则更普遍的概念，但两者其实关系是很紧密的。遵守这个原则的类容易具有很高的凝聚力，而且比背负许多责任的低内聚类更容易维护。</span></div></li></ul><div><span style="font-size: 12pt; font-weight: bold;">二、开闭原则</span></div><ul><li><div><span style="font-size: 12pt;">定义</span></div></li><li><div><span style="font-size: 16px;">软件实体（类，模块，函数等等）应该可以扩展，但是不可以修改。</span></div></li><li><div><span style="font-size: 12pt;">一个软件实体如类、模块和函数应该对扩展开放，对修改关闭。</span></div></li><li><div><span style="font-size: 12pt;">软件实体应尽量在不修改原有代码的情况下进行扩展。</span></div></li><li><div><span style="font-size: 12pt;">开闭原则是Java世界里最基础的设计原则，它指导我们如何建立一个稳定的、灵活的系统。</span></div></li><li><div><span style="font-size: 12pt;">开闭原则是面向对象的可复用设计的第一块基石，它是最重要的面向对象设计原则。</span></div></li><li><div><span style="font-size: 12pt;">其含义是说一个软件实体应该通过扩展来实现变化，而不是通过修改已有的代码来实现变化。</span></div></li><li><div><span style="font-size: 12pt;">任何软件都需要面临一个很重要的问题，即它们的需求会随时间的推移而发生变化。当软件系统需要面对新的需求时，我们应该尽量保证系统的设计框架是稳定的。如果一个软件设计符合开闭原则，那么可以非常方便地对系统进行扩展，而且在扩展时无须修改现有代码，使得软件系统在拥有适应性和灵活性的同时具备较好的稳定性和延续性。随着软件规模越来越大，软件寿命越来越长，软件维护成本越来越高，设计满足开闭原则的软件系统也变得越来越重要。</span></div></li><li><div><span style="font-size: 12pt;">为了满足开闭原则，需要对系统进行抽象化设计，抽象化是开闭原则的关键。可以为系统定义一个相对稳定的抽象层，而将不同的实现行为移至具体的实现层中完成。如果需要修改系统的行为，无须对抽象层进行任何改动，只需要增加新的具体类来实现新的业务功能即可，实现在不修改已有代码的基础上扩展系统的功能，达到开闭原则的要求。</span></div></li><li><div><span style="font-size: 16px;">软件实体的定义</span></div></li><li><div><span style="font-size: 12pt;">项目或软件产品中按照一定的逻辑规则划分的模块。</span></div></li><li><div><span style="font-size: 16px;">抽象和类</span></div></li><li><div><span style="font-size: 16px;">方法</span></div></li><li><div><span style="font-size: 12pt;">一个软件产品只要在生命期内，都会发生变化，既然变化是一个既定的事实，我们就应该在设计时尽量适应这些变化，以提高项目的稳定性和灵活性，真正实现“拥抱变化”。开闭原则告诉我们应尽量通过扩展软件实体的行为来实现变化，而不是通过修改已有的代码来完成变化，它是为软件实体的未来事件而制定的对现行开发设计进行约束的一个原则。</span></div></li><li><div><span style="font-size: 12pt;">在业务规则改变的情况下高层模块必须有部分改变以适应新业务，改变要尽量地少，防止变化风险的扩散。</span></div></li><li><div><span style="font-size: 12pt;">开闭原则对扩展开放，对修改关闭，并不意味着不做任何修改，低层模块的变更，必然要有高层模块进行耦合，否则就是一个孤立无意义的代码片段。</span></div></li><li><div><span style="font-size: 16px;">虽然似乎有点矛盾，但是的确有一些技术可以允许在不直接修改代码的情况下对其进行扩展。</span></div></li><li><div><span style="font-size: 16px;">在选择需要被扩展的代码部分时要小心。每个地方都采用开放-关闭原则，是一种浪费，也没有必要，还会导致代码变得复杂且难以理解。</span></div></li><li><div><span style="font-size: 16px;">变化归纳为以下三种类型：</span></div></li><li><div><span style="font-size: 16px;">逻辑变化</span></div></li></ul><div style="margin-left: 80px;"><span style="font-size: 12pt;">只变化一个逻辑，而不涉及其他模块，</span><span style="font-size: 12pt;">可以通过修改原有类中的方法的方式来完成，前提条件是所有依赖或关联类都按照相同的逻辑处理。</span></div><ul><li><div><span style="font-size: 16px;">子模块变化</span></div></li></ul><div style="margin-left: 80px;"><span style="font-size: 12pt;">一个模块变化，会对其他的模块产生影响，特别是一个低层次的模块变化必然引起高层模块的变化，因此在通过扩展完成变化时，高层次的模块修改是必然的。</span></div><ul><li><div><span style="font-size: 16px;">可见视图变化</span></div></li></ul><div style="margin-left: 80px;"><span style="font-size: 12pt;">可见视图是提供给客户使用的界面，</span><span style="font-size: 12pt;">该部分的变化一般会引</span></div><div style="margin-left: 80px;"><span style="font-size: 12pt;">起连锁反应，</span> <span style="font-size: 12pt;">最司空见惯的是业务耦合变化，</span><span style="font-size: 12pt;">但还是可以通过扩展来完成变化，这就要看我们原有的设计是否灵活。</span></div><ul><li><div><span style="font-size: 12pt;">应对变化逻辑：</span></div></li><li><div><span style="font-size: 12pt;">首先是我们有一个还算灵活的设计；</span></div></li><li><div><span style="font-size: 12pt;">然后有一个需求变化，我们通过扩展一个子类拥抱了变化；</span></div></li><li><div><span style="font-size: 12pt;">最后把子类投入运行环境中， 新逻辑正式投产。</span></div></li><li><div><span style="font-size: 12pt;">放弃修改历史的想法吧，一个项目的基本路径应该是这样的：项目开发、重构、测试、投产、运维，其中的重构可以对原有的设计和代码进行修改，运维尽量减少对原有代码的修改，保持历史代码的纯洁性，提高系统的稳定性。</span></div></li><li><div><span style="font-size: 16px;">在软件设计模式中，不能修改，但可以扩展的思想是最重要的一种设计原则，这就是开放-封闭原则，也叫开-闭原则。</span></div></li><li><div><span style="font-size: 16px;">在做任何系统的时候，都不要指望一开始就需求确定，就再也不会变化，这是不现实也不科学的想法，而既然需求是一定会变化的，开闭原则指导我们：</span></div></li><li><div><span style="font-size: 16px;">在面对需求的变化时，设计的软件可以相对容易的修改，不至于说新需求一来，就要把整个程序推倒重来</span></div></li><li><div><span style="font-size: 16px;">如何设计才能面对需求的改变却可以保持相对稳定，从而使得系统可以在第一个版本之后不断推出新的版本</span></div></li><li><div><span style="font-size: 16px;">设计软件要容易维护又不容易出问题的最好办法就是多扩展，少修改。</span></div></li><li><div><span style="font-size: 12pt;">开放封闭原则的意思就是说，你设计的时候，时刻要考虑，尽量让这个类是足够好，写好了就不要去修改了，如果新需求来，我们增加一些类就完事了，原来的代码能不动则不动。</span></div></li><li><div><span style="font-size: 12pt;">绝对的对修改关闭是不可能的。无论模块是多么的“封闭”，都会存在一些无法对之封闭的变化。既然不可能完全封闭，设计人员必须对于他设计的模块应该对哪种变化封闭做出选择。他必须先猜测出最有可能发生的变化种类，然后构造抽象来隔离那些变化。</span></div></li><li><div><span style="font-size: 12pt;">我们是很难预先猜测，但我们却可以在发生小变化时，就及早去想办法应对发生更大变化的可能。也就是说，等到变化发生时立即采取行动。</span></div></li><li><div><span style="font-size: 12pt;">在我们最初编写代码时，假设变化不会发生。当变化发生时，我们就创建抽象来隔离以后发生的同类变化。</span></div></li><li><div><span style="font-size: 12pt;">面对需求，对程序的改动是通过增加新代码进行的，而不是更改现有的代码。</span><span style="font-size: 12pt;">这就是‘开放封闭原则”的精神所在。</span></div></li><li><div><span style="font-size: 12pt;">并不是什么时候应对变化都是容易的。我们希望的是在开发工作展开不久就知道可能发生的变化。查明可能发生的变化所等待的时间越长，要创建正确的抽象就越困难。</span></div></li><li><div><span style="font-size: 12pt;">开放封闭原则是面向对象设计的核心所在。遵循这个原则可以带来面向对象技术所声称的巨大好处，也就是可维护、可扩展、可复用、灵活性好。开发人员应该仅对程序中呈现出频繁变化的那些部分做岀抽象，然而，对于应用程序中的每个部分都刻意地进行抽象同样不是一个好主意。拒绝不成熟的抽象和抽象本身一样重要。</span></div></li><li><div><span style="font-size: 16px;">使用开闭原则的理由：</span></div></li><li><div><span style="font-size: 12pt;">首先，开闭原则非常著名，只要是做面向对象编程的，甭管是什么语言，在开发时都会提及开闭原则。</span></div></li><li><div><span style="font-size: 12pt;">其次，开闭原则是最基础的一个原则，其他</span><span style="font-size: 12pt;">原则都是开闭原则的具体形态，也就是说其他五个原则就是指导设计的工具和方法，而开闭原则才是其精神领袖。</span></div></li><li><div><span style="font-size: 16px;">最后，开闭原则是最重要的。</span></div></li><li><div><span style="font-size: 16px;">开闭原则</span><span style="font-size: 12pt;">对测试的影响</span></div></li></ul><div style="margin-left: 120px;"><span style="font-size: 12pt;">有变化提出时，我们就需要考虑一下，原有的健壮代码是否可以不修改，仅仅通过扩展实现变化呢？否则，就需要把原有的测试过程回笼一遍，需要进行单元测试、功能测试、集成测试甚至是验收测试，现在虽然在大力提倡自动化测试工具，但是仍然代替不了人工的测试工作。</span></div><ul><li><div><span style="font-size: 12pt;">开闭原则可以提高复用性</span></div></li></ul><div style="margin-left: 120px;"><span style="font-size: 12pt;">在面向对象的设计中，所有的逻辑都是从原子逻辑组合而来的，而不是在一个类中独立实现一个业务逻辑。只有这样代码才可以复用，粒度越小，被复用的可能性就越大。那为什么要复用呢？减少代码量，避免相同的逻辑分散在多个角落。</span><span style="font-size: 12pt;">怎么才能提高复用率呢？缩小逻辑粒度，直到一个逻辑不可再拆分为止。</span></div><ul><li><div><span style="font-size: 16px;">开闭原则课提高可维护性</span></div></li></ul><div style="margin-left: 120px;"><span style="font-size: 12pt;">维护人员最乐意做的事情就是扩展一个类，而不是修改一个类，甭管原有的代码写得多么优秀还是多么糟糕，让维护人员读懂原有的代码，然后再修改，是一件很痛苦的事情。</span></div><ul><li><div><span style="font-size: 16px;">面向对象的开发要求</span></div></li></ul><div style="margin-left: 120px;"><span style="font-size: 12pt;">万物皆对象，我们需要把所有的事物都抽象成对象，然后针对对象进行操作。</span><span style="font-size: 12pt;">这就需要在设计之初考虑到所有可能变化的因素，然后留下接口，等待“可能”转变为“现实”。</span></div><ul><li><div><span style="font-size: 12pt;">开闭原则是一个非常虚的原则，前面5个原则是对开闭原则的具体解释，但是开闭原则并不局限于这么多，它“虚”得没有边界。</span></div></li><li><div><span style="font-size: 16px;">如何应用开闭原则：</span></div></li><li><div><span style="font-size: 16px;">抽象约束</span></div></li></ul><div style="margin-left: 80px;"><span style="font-size: 12pt;">抽象是对一组事物的通用描述，没有具体的实现，也就表示它可以有非常多的可能性，可以跟随需求的变化而变化。因此，通过接口或抽象类可以约束一组可能变化的行为，并且能够实现对扩展开放，其包含三层含义：</span></div><ul><li><div><span style="font-size: 12pt;">第一，通过接口或抽象类约束扩展，对扩展进行边界限定，不允许出现在接口或抽象类中不存在的public方法；</span></div></li><li><div><span style="font-size: 12pt;">第二，参数类型、引用对象尽量使用接口或者抽象类，而不是实现类；</span></div></li><li><div><span style="font-size: 12pt;">第三，抽象层尽量保持稳定，一旦确定即不允许修改。</span></div></li></ul><div style="margin-left: 80px;"><span style="font-size: 12pt;">要实现对扩展开放，首要的前提条件就是抽象约束。</span></div><ul><li><div><span style="font-size: 16px;">元数据控制模块行为</span></div></li></ul><div style="margin-left: 80px;"><span style="font-size: 12pt;">尽量使用元数据来控制程序的行为，减少重复开发。</span></div><div style="margin-left: 80px;"><span style="font-size: 16px;">元数据：</span><span style="font-size: 12pt;">用来描述环境和数据的数据，通俗地说就是配置参数，参数可以从文件中获得，也可以从数据库中获得。</span></div><ul><li><div><span style="font-size: 16px;">制定项目章程</span></div></li></ul><div style="margin-left: 80px;"><span style="font-size: 12pt;">在一个团队中，建立项目章程是非常重要的，因为章程中指定了所有人员都必须遵守的约定，对项目来说，约定优于配置。</span></div><div style="margin-left: 80px;"><span style="font-size: 12pt;">需要项目内约束，每个项目成员都必须遵守，该方法需要一个团队有较高的自觉性，需要一个较长时间的磨合，一旦项目成员都熟悉这样的规则，比通过接口或抽象类进行约束效率更高，而且扩展性一点也没有减少。</span></div><ul><li><div><span style="font-size: 16px;">封装变化</span></div></li></ul><div style="margin-left: 80px;"><span style="font-size: 12pt;">对变化的封装包含两层含义：</span></div><ul><li><div><span style="font-size: 12pt;">第一，将相同的变化封装到一个接口或抽象类中；</span></div></li><li><div><span style="font-size: 12pt; color: unset; font-family: unset;">第二，将不同的变化封装到不同的接口或抽象类中，不应该有两个不同的变化出现在同一个接口或抽象类中。</span></div></li></ul><div style="margin-left: 80px;"><span style="font-size: 12pt;">封装变化，也就是受保护的变化（protectedvariations），找出预计有变化或不稳定的点，我们为这些变化点创建稳定的接口，准确地讲是封装可能发生的变化。</span></div><div style="margin-left: 80px;"><span style="font-size: 12pt;">23个设计模式都是从各个不同的角度对变化进行封装的。</span></div><ul><li><div><span style="font-size: 12pt;">开闭原则是6大设计原则的重中之重，是最基础的原则，是其他6大原则的精神领袖。</span></div></li><li><div><span style="font-size: 16px;">使用开闭原则时要注意的几个问题：</span></div></li><li><div><span style="font-size: 12pt;">开闭原则也只是一个原则</span></div></li></ul><div style="margin-left: 80px;"><span style="font-size: 12pt;">开闭原则只是精神口号，实现拥抱变化的方法非常多，并不局限于这6大设计原则，但是遵循这6大设计原则基本上可以应对大多数变化。因此，我们在项目中应尽量采用这6大原则，适当时候可以进行扩充。</span></div><div style="margin-left: 80px;"><span style="font-size: 16px;">也就是说，原则是死的，人是活的，要根据具体情况灵活运用。</span></div><ul><li><div><span style="font-size: 12pt;">项目规章非常重要</span></div></li></ul><div style="margin-left: 80px;"><span style="font-size: 12pt;">如果你是一位项目经理或架构师，应尽量让自己的项目成员稳定，稳定后才能建立高效的团队文化，章程是一个团队所有成员共同的知识结晶，也是所有成员必须遵守的约定。优秀的章程能带给项目带来非常多的好处，如提高开发效率、降低缺陷率、提高团队士气、提高技术成员水平，等等。</span></div><ul><li><div><span style="font-size: 12pt;">预知变化</span></div></li></ul><div style="margin-left: 80px;"><span style="font-size: 12pt;">在实践中过程中，架构师或项目经理一旦发现有发生变化的可能，或者变化曾经发生过，则需要考虑现有的架构是否可以轻松地实现这一变化。架构师设计一套系统不仅要符合现有的需求，还要适应可能发生的变化，这才是一个优良的架构。</span></div><ul><li><div><span style="font-size: 12pt;">开闭原则是一个终极目标，任何人包括大师级人物都无法百分之百做到，但朝这个方向努力，可以非常显著地改善一个系统的架构，真正做到“拥抱变化”。</span></div></li><li><div><span style="font-size: 12pt;">在软件开发中，一般不把对配置文件的修改认为是对系统源代码的修改。如果一个系统在扩展时只涉及到修改配置文件，而原有代码没有做任何修改，该系统即可认为是一个符合开闭原则的系统。</span></div></li></ul><div><span style="font-size: 12pt; font-weight: bold;">三、里氏代换原则</span></div><ul><li><div><span style="font-size: 16px;"><span style="font-size: 16px;">定义</span></span></div></li><li><div><span style="font-size: 12pt;">严格表述：如果对每一个类型为S的对象o1，都有类型为T的对象o2，使得以T定义的所有程序P在所有的对象o1代换o2时，程序P的行为没有变化，那么类型S是类型T的子类型。</span></div></li><li><div><span style="font-size: 16px;">通俗版定义：</span><span style="font-size: 12pt;">所有引用基类（父类）的地方必须能透明地使用其子类的对象。</span></div></li><li><div><span style="font-size: 16px;">一个软件实体如果使用的是一个父类的话，那么一定适用于其子类，而且它察觉不出父类对象和子类对象的区别。也就是说，在软件里面，把父类都替换成它的子类，程序的行为没有变化。</span></div></li><li><div><span style="font-size: 12pt;">里式替换原则是用来指导，继承关系中子类该如何设计的一个原则。</span></div></li><li><div><span style="font-size: 12pt;">子类对象（object of subtype/derived class）能够替换程序（program）中父类对象（object of base/parent class）出现的任何地方，并且保证原来程序的逻辑行为（behavior）不变及正确性不被破坏。</span></div></li><li><div><span style="font-size: 12pt;">实际上，里式替换原则还有另外一个更加能落地、更有指导意义的描述，那就是“Design By Contract”，中文翻译就是“按照协议来设计”。</span></div><div><span style="font-size: 12pt;">子类在设计的时候，要遵守父类的行为约定（或者叫协议）。父类定义了函数的行为约定，那子类可以改变函数的内部实现逻辑，但不能改变函数原有的行为约定。这里的行为约定包括：函数声明要实现的功能；对输入、输出、异常的约定；甚至包括注释中所罗列的任何特殊说明。实际上，定义中父类和子类之间的关系，也可以替换成接口和实现类之间的关系。</span></div></li><li><div><span style="font-size: 12pt;">判断子类的设计实现是否违背里式替换原则，还有一个小窍门，那就是拿父类的单元测试去验证子类的代码。如果某些单元测试运行失败，就有可能说明，子类的设计实现没有完全地遵守父类的约定，子类有可能违背了里式替换原则。</span></div></li><li><div><span style="font-size: 16px;">简单的说，子类型必须能够替换掉他们的父类。</span></div></li><li><div><span style="font-size: 16px;">子类继承父类，所以子类可以以父类的身份出现。</span></div></li><li><div><span style="font-size: 12pt;">里氏代换原则告诉我们，在软件中将一个基类对象替换成它的子类对象，程序将不会产生任何错误和异常，即</span><span style="font-size: 16px; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; color: rgb(0, 0, 0); font-family: 微软雅黑; font-variant-caps: normal; font-variant-ligatures: normal;">只要父类能出现的地方子类就可以出现，</span><span style="font-size: 16px; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; color: rgb(0, 0, 0); font-family: 微软雅黑; font-variant-caps: normal; font-variant-ligatures: normal;">使用者可能根本就不需要知道是父类还是子类。</span><span style="font-size: 12pt;">反过来则不成立，如果一个软件实体使用的是一个子类对象的话，那么它不一定能够使用基类对象，即</span><span style="font-size: 16px; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; color: rgb(0, 0, 0); font-family: 微软雅黑; font-variant-caps: normal; font-variant-ligatures: normal;">有子类出现的地方，父类未必就能适应。</span></div></li><li><div><span style="font-size: 12pt;">虽然从定义描述和代码实现上来看，多态和里式替换有点类似，但它们关注的角度是不一样的。多态是面向对象编程的一大特性，也是面向对象编程语言的一种语法。它是一种代码实现的思路。而里式替换是一种设计原则，是用来指导继承关系中子类该如何设计的，子类的设计要保证在替换父类的时候，不改变原有程序的逻辑以及不破坏原有程序的正确性。</span></div></li><li><div><span style="font-size: 16px;">正因为有了里氏代换原则，使得继承复用成为了可能，只有当子类可以替换掉父类，软件单位的功能不受到影响时，父类才能真正被复用，而子类也能够在父类的基础上增加新的行为。</span></div></li><li><div><span style="font-size: 16px;">正因为有了里氏代换原则，才使得开放-封闭成为可能。正是由于子类型的可替换性才使得使用父类类型的模块在无需修改的情况下就可以扩展。</span><span style="font-size: 12pt; color: unset; font-family: unset;">里氏代换原则是实现开闭原则的重要方式之一，由于使用基类对象的地方都可以使用子类对象，因此在程序中尽量使用基类类型来对对象进行定义，而在运行时再确定其子类类型，用子类对象来替换父类对象。</span></div></li><li><div><span style="font-size: 16px;">优缺点</span></div></li><li><div><span style="font-size: 12pt;">在面向对象的语言中，继承是必不可少的、非常优秀的语言机制，它有如下优点：</span></div></li><li><div><span style="font-size: 12pt;">代码共享， 减少创建类的工作量， 每个子类都拥有父类的方法和属性；</span></div></li><li><div><span style="font-size: 12pt;">提高代码的重用性；</span></div></li><li><div><span style="font-size: 12pt;">子类可以形似父类， 但又异于父类；</span></div></li><li><div><span style="font-size: 12pt;">提高代码的可扩展性，实现父类的方法就可以“为所欲为”了，很多开源框架的扩展接口都是通过继承父类来完成的；</span></div></li><li><div><span style="font-size: 12pt;">提高产品或项目的开放性。</span></div></li><li><div><span style="font-size: 16px;">继承的缺点如下</span></div></li><li><div><span style="font-size: 12pt;">继承是侵入性的。只要继承，就必须拥有父类的所有属性和方法；</span></div></li><li><div><span style="font-size: 12pt;">降低代码的灵活性。子类必须拥有父类的属性和方法，让子类自由的世界中多了些约束；</span></div></li><li><div><span style="font-size: 12pt;">增强了耦合性。当父类的常量、变量和方法被修改时，需要考虑子类的修改，而且在缺乏规范的环境下，这种修改可能带来非常糟糕的结果——大段的代码需要重构。</span></div></li><li><div><span style="font-size: 12pt;">从整体上来看，继承利大于弊，如何让“利”的因素发挥最大的作用，同时减少“弊”带来的麻烦，解决方案是引入里氏替换原则。</span></div></li><li><div><span style="font-size: 12pt;">在使用里氏代换原则时需要注意如下几个问题：</span></div></li><li><div><span style="font-size: 12pt;">子类的所有方法必须在父类中声明，或子类必须实现父类中声明的所有方法。根据里氏代换原则，为了保证系统的扩展性，在程序中通常使用父类来进行定义，如果一个方法只存在子类中，在父类中不提供相应的声明，则无法在以父类定义的对象中使用该方法。</span></div></li><li><div><span style="font-size: 12pt;">我们在运用里氏代换原则时，尽量把父类设计为抽象类或者接口，让子类继承父类或实现父接口，并实现在父类中声明的方法，运行时，子类实例替换父类实例，我们可以很方便地扩展系统的功能，同时无须修改原有子类的代码，增加新的功能可以通过增加一个新的子类来实现。里氏代换原则是开闭原则的具体实现手段之一。</span></div></li><li><div><span style="font-size: 12pt;">Java语言中，在编译阶段，Java编译器会检查一个程序是否符合里氏代换原则，这是一个与实现无关的、纯语法意义上的检查，但Java编译器的检查是有局限的。</span></div></li><li><div><span style="font-size: 12pt;">在定义成员变量、定义局部变量、确定方法返回类型时都可使用里氏代换原则。针对基类编程，在程序运行时再确定具体子类。</span></div></li><li><div><span style="font-size: 12pt;">里氏替换原则为良好的继承定义了一个规范，一句简单的定义包含了4层含义。</span></div></li><li><div><span style="font-size: 12pt;">子类必须完全实现父类的方法</span></div></li><li><div><span style="font-size: 12pt;">我们在做系统设计时，经常会定义一个接口或抽象类，然后编码实现，调用类则直接传入接口或抽象类，其实这里已经使用了里氏替换原则。</span></div></li><li><div><span style="font-size: 12pt; color: unset; font-family: unset;">在类中调用其他类时务必要使用父类或接口，如果不能使用父类或接口，则说明类的设计已经违背了LSP原则。</span></div></li><li><div><span style="font-size: 16px;">Java的三大特征是封装、继承、多态。继承就是告诉你拥有父类的方法和属性，然后可以重写父类的方法，按继承原则，在具体应用场景中要考虑子类是否能够完整的实现父类的业务。</span></div></li><li><div><span style="font-size: 12pt;">如果子类不能完整地实现父类的方法，或者父类的某些方法在子类中已经发生“畸变”，则建议断开父子继承关系，采用依赖、聚集、组合等关系代替继承。</span></div></li><li><div><span style="font-size: 12pt;">子类可以有自己的个性</span></div></li><li><div><span style="font-size: 12pt;">子类当然可以有自己的行为和外观了，也就是方法和属性，但里氏替换原则可以正着用，但是不能反过来用。在子类出现的地方，父类未必就可以胜任。</span></div></li><li><div><span style="font-size: 12pt;">这也是大家经常说的向下转型（downcast）是不安全的，从里氏替换原则来看，就是有子类出现的地方父类未必就可以出现。</span></div></li><li><div><span style="font-size: 12pt;">覆盖或实现父类的方法时输入参数可以被放大</span></div></li><li><div><span style="font-size: 12pt;">方法中的输入参数称为前置条件，</span><span style="font-size: 12pt;">里氏替换原则也要求制定一个契约，就是父类或接口，这种设计方法也叫做DesignbyContract（契约设计），</span><span style="font-size: 12pt;">契约制定了，也就同时制定了前置条件和后置条件，前置条件就是你要让我执行，就必须满足我的条件；后置条件就是我执行完了需要反馈，标准是什么。</span></div></li><li><div><span style="font-size: 16px;">重载和覆写</span></div></li><li><div><span style="font-size: 16px;">重载：子类和父类方法名相同，但方法的输入参数不同。【Java中似乎没有覆盖的说法，当子类和父类方法名相同，输入参数不同的时候，子类不会隐藏父类的方法，只是简单的重载父类的方法，两者都可以使用】</span></div></li><li><div><span style="font-size: 16px;">覆写：子类与父类方法名相同，方法的输入参数也相同，这是多态，虚函数的覆写。</span></div></li><li><div><span style="font-size: 16px;">如想要里氏代换原则成立（即父类可以用子类代替），子类应覆写父类的方法，也可以重载这个方法，前提是子类要扩大方法的前置条件，就是输入参数的类型宽于父类的类型覆盖范围。</span></div></li><li><div><span style="font-size: 12pt;">子类在没有覆写父类的方法的前提下，子类方法被执行了，这会引起业务逻辑混乱，因为在实际应用中父类一般都是抽象类，子类是实现类，你传递一个这样的实现类就会“歪曲”了父类的意图，引起一堆意想不到的业务逻辑混乱，所以子类中方法的前置条件必须与超类中被覆写的方法的前置条件相同或者更宽松。</span></div></li><li><div><span style="font-size: 12pt;">覆写或实现父类的方法时输出结果可以被缩小</span></div></li><li><div><span style="font-size: 12pt;">父类的一个方法的返回值是一个类型T，子类的相同方法（重载或覆写）的返回值为S，那么里氏替换原则就要求S必须小于等于T，也就是说，要么S和T是同一个类型，要么S是T的子类。</span></div></li><li><div><span style="font-size: 12pt;">如果是覆写，父类和子类的同名方法的输入参数是相同的，两个方法的范围值S小于等于T，这是覆写的要求。</span></div></li><li><div><span style="font-size: 12pt;">如果是重载，则要求方法的输入参数类型或数量不相同，在里氏替换原则要求下，写的这个方法是不会被调用的。</span></div></li><li><div><span style="font-size: 12pt;">采用里氏替换原则的目的就是增强程序的健壮性，版本升级时也可以保持非常好的兼容性。即使增加子类，原有的子类还可以继续运行。</span></div></li><li><div><span style="font-size: 12pt;">在实际项目中，每个子类对应不同的业务含义，使用父类作为参数，传递不同的子类完成不同的业务逻辑。</span></div></li><li><div><span style="font-size: 12pt;">在项目中，采用里氏替换原则时，尽量避免子类的“个性”，一旦子类有“个性”，这个子类和父类之间的关系就很难调和了，把子类当做父类使用，子类的“个性”被抹杀；把子类单独作为一个业务来使用，则会让代码间的耦合关系变得扑朔迷离，不满足开闭原则的要求。</span></div></li></ul><div><span style="font-size: 12pt; font-weight: bold;">四、依赖倒转原则</span></div><ul><li><div><span style="font-size: 16px;">定义</span></div></li><li><div><span style="font-size: 16px;">高层模块不应该依赖低层模块，两个都应该依赖抽象。</span></div></li><li><div><span style="font-size: 16px;">抽象不应该依赖细节，细节应该依赖抽象。</span></div></li><li><div><span style="font-size: 16px;">针对接口编程，不要针对实现编程。</span></div></li><li><div><span style="font-size: 16px;">代码里减少对于具体类的依赖是件好事。</span></div></li><li><div><span style="font-size: 16px;">解释</span></div></li><ul><li><div><span style="font-size: 16px;">高层模块和低层模块</span></div></li></ul></ul><div style="margin-left: 80px;"><div><span style="font-size: 12pt;">高层模块和低层模块容易理解，每一个逻辑的实现都是由原子逻辑组成的，不可分割的原子逻辑就是低层模块，原子逻辑的再组装就是高层模块。</span></div><div><span style="font-size: 12pt;">所谓高层模块和低层模块的划分，简单来说就是，在调用链上，调用者属于高层，被调用者属于低层。</span></div><div><span style="font-size: 16px;">高层组件是由其他低层组件定义其行为的类。</span></div></div><ul><ul><li><div><span style="font-size: 16px;">抽象和细节</span></div></li></ul></ul><div style="margin-left: 80px;"><span style="font-size: 12pt;">在Java语言中，抽象就是指接口或抽象类，两者都是不能直接被实例化的；细节就是实现类，实现接口或继承抽象类而产生的类就是细节，其特点就是可以直接被实例化，也就是可以加上一个关键字new产生一个对象。</span></div><ul><ul><li><div><span style="font-size: 16px; color: unset; font-family: unset;">倒置</span></div></li></ul></ul><div style="margin-left: 80px;"><span style="font-size: 16px; color: unset; font-family: unset;">倒置指的是和一般OO设计的思考方式完全相反。低层组件依赖高层的抽象，高层组件也依赖相同的抽象。</span></div><div style="margin-left: 80px;"><span style="font-size: 12pt;">先说“正置”是什么意思，依赖正置就是类间的依赖是实实在在的实现类间的依赖，也就是面向实现编程，这也是正常人的思维方式。</span></div><div style="margin-left: 80px;"><span style="font-size: 12pt;">而编写程序需要的是对现实世界的事物进行抽象，抽象的结果就是有了抽象类和接口，然后我们根据系统设计的需要产生了抽象间的依赖，代替了人们传统思维中的事物间的依赖，“倒置”就是从这里产生的。</span></div><ul><li><div><span style="font-size: 12pt;">如果说开闭原则是面向对象设计的目标的话，那么依赖倒转原则就是面向对象设计的主要实现机制之一，它是系统抽象化的具体实现。</span></div></li><li><div><span style="font-size: 16px;">依赖倒置原则在Java语言中的表现：</span></div></li><li><div><span style="font-size: 12pt;">模块间的依赖通过抽象发生，实现类之间不发生直接的依赖关系，其依赖关系是通过接口或抽象类产生的；</span></div></li><li><div><span style="font-size: 16px;">接口或者抽象类不依赖于实现类；</span></div></li><li><div><span style="font-size: 16px;">实现类依赖接口或抽象类。</span></div></li><li><div><span style="font-size: 16px;">实际上，依赖倒置原则更加精简的定义就是“面向接口编程”。</span></div></li><li><div><span style="font-size: 12pt;">采用依赖倒置原则可以减少类间的耦合性，提高系统的稳定性，降低并行开发引起的风险，提高代码的可读性和可维护性。</span></div></li><li><div><span style="font-size: 16px;">提高系统稳定性</span></div></li></ul><div style="margin-left: 80px;"><span style="font-size: 12pt;">设计是否具备稳定性，只要适当地“松松土”，观察“设计的蓝图”是否还可以茁壮地成长就可以得出结论，稳定性较高的设计，在周围环境频繁变化的时候，依然可以做到“我自岿然不动”。</span></div><ul><li><div><span style="font-size: 12pt;">降低并行开发引起的风险</span></div></li></ul><div style="margin-left: 80px;"><span style="font-size: 12pt;">并行开发最大的风险就是风险扩散，本来只是一段程序的错误或异常，逐步波及一个功能，一个模块，甚至到最后毁坏了整个项目。</span></div><div style="margin-left: 80px;"><span style="font-size: 12pt; color: unset; font-family: unset;">接口只是一个抽象化的概念，是对一类事物的最抽象描述，具体的实现代码由相应的实现类来完成。如果高层业务逻辑对低层模块的依赖都建立在抽象上，在新增加低层模块时，只修改了业务场景类，也就是高层模块，对其他低层模块不需要做任何修改，业务就可以运行，这就把“变更”引起的风险扩散降到最低。</span></div><div style="margin-left: 80px;"><span style="font-size: 12pt;">两个类之间有依赖关系，只要制定出两者之间的接口（或抽象类）就可以独立开发了，而且项目之间的单元测试也可以独立地运行，而TDD（Test-Driven Development，测试驱动开发）开发模式就是依赖倒置原则的最高级应用。可以引入一个JMock工具，其最基本的功能是根据抽象虚拟一个对象进行测试。</span></div><div style="margin-left: 80px;"><span style="font-size: 12pt;">从这一点来看，两个相互依赖的对象可以分别进行开发，孤立地进行单元测试，进而保证并行开发的效率和质量。</span></div><div style="margin-left: 80px;"><span style="font-size: 12pt;">测试驱动开发，先写好单元测试类，然后再写实现类，这对提高代码的质量有非常大的帮助，特别适合研发类项目或在项目成员整体水平比较低的情况下采用。</span></div><ul><li><div><span style="font-size: 16px;">面向过程开发时，为了使得常用代码可以复用，一般都会把这些常用代码写成许许多多函数的程序库，这样我们在做新项目的时候，去调用这些低层的函数就可以了。这就叫做高层模块依赖低层模块。</span></div></li></ul><div style="margin-left: 40px;"><span style="font-size: 16px;">问题也就出现在这里，我们要做新项目的时候，发现业务逻辑的高层模块都是一样的，但客户却希望使用不同的低层模块，我们希望能再次利用这些高层模块，但高层模块都是与低层绑定在一起的，没有办法复用这些高层模块，这就非常糟糕了。</span></div><div style="margin-left: 40px;"><span style="font-size: 16px;">而如果不管高层模块还是低层模块，它们都依赖于抽象，具体一点就是接口和抽象类，只要接口是稳定的，那么任何一个的更改都不用担心其他受到影响，这就使得无论高层模块还是低层模块都可以很容易的被复用。</span></div><ul><li><div><span style="font-size: 12pt;">依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中，尽量引用层次高的抽象层类，即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明，以及数据类型的转换等，而不要用具体类来做这些事情。为了确保该原则的应用，一个具体类应当只实现接口或抽象类中声明过的方法，而不要给出多余的方法，否则将无法调用到在子类中增加的新方法。</span></div></li><li><div><span style="font-size: 12pt;">抽象是对实现的约束，对依赖者而言，也是一种契约，不仅仅约束自己，还同时约束自己与外部的关系，其目的是保证所有的细节不脱离契约的范畴，确保约束双方按照既定的契约（抽象）共同发展，只要抽象这根基线在，细节就脱离不了这个圈圈，始终让你的对象做到“言必信，行必果”。</span></div></li><li><div><span style="font-size: 16px;">依赖是可以传递的，但只要做到抽象依赖，即使是多层的依赖传递也无所畏惧。</span></div></li><li><div><span style="font-size: 12pt;">在实现依赖倒转原则时，我们需要针对抽象层编程，而将具体类的对象通过依赖注入(DependencyInjection,DI)的方式注入到其他对象中，依赖注入是指当一个对象要与其他对象发生依赖关系时，通过抽象来注入所依赖的对象。</span><span style="font-size: 16px;">对象的依赖关系有三种方式传递：</span></div></li><li><div><span style="font-size: 16px;">构造注入，</span><span style="font-size: 12pt;">通过构造函数来传入具体类的对象</span></div></li><li><div><span style="font-size: 12pt;">在类中通过构造函数声明依赖对象</span><span style="font-size: 12pt;">。</span></div></li><li><div><span style="font-size: 12pt;">设置注入，</span><span style="font-size: 12pt;">通过Setter方法来传入具体类的对象</span></div></li><li><div><span style="font-size: 12pt;">在抽象中设置Setter方法声明依赖关系。</span></div></li><li><div><span style="font-size: 12pt;">接口注入，接口声明依赖对象</span></div></li><li><div><span style="font-size: 12pt;">在接口中声明的业务方法来传入具体类的对象。</span><span style="font-size: 12pt;">这些方法在定义时使用的是抽象类型，在运行时再传入具体类型的对象，由子类对象来覆盖父类对象。</span></div></li><li><div><span style="font-size: 16px;">依赖倒转其实可以说是面向对象设计的标志，用哪种语言来编写程序不重要，如果编写时考虑的都是如何针对抽象编程而不是针对细节编程，即程序中所有的依赖关系都是终止于抽象类或者接口，那就是面向对象的设计，反之那就是过程化的设计。</span></div></li><li><div><span style="font-size: 16px;">依赖倒置原则的本质就是通过抽象（接口或者抽象类）使各个类或模块的实现彼此独立，不互相影响，实现模块间的松耦合。</span></div></li><li><div><span style="font-size: 16px;">在项目中应用依赖倒置原则需要遵循：</span></div></li><li><div><span style="font-size: 12pt;">每个类尽量都有接口或抽象类，或者抽象类和接口两者都具备</span></div></li></ul><div style="margin-left: 80px;"><span style="font-size: 12pt;">这是依赖倒置的基本要求，接口和抽象类都是属于抽象的，有了抽象才可能依赖倒置。</span></div><ul><li><div><span style="font-size: 12pt;">变量的表面类型</span><span style="font-size: 12pt; font-weight: bold;">尽量</span><span style="font-size: 12pt;">是接口或者是抽象类</span></div></li><li><div><span style="font-size: 12pt;">任何类都不应该从具体类派生</span></div></li><li><div><span style="font-size: 16px;">尽量不要覆写基类的方法</span></div></li></ul><div style="margin-left: 80px;"><span style="font-size: 12pt;">如果基类是一个抽象类，而且这个方法已经实现了，子类尽量不要覆写。类间依赖的是抽象，覆写了抽象方法，对依赖的稳定性会产生一定的影响。【会产生对结果不确切的预期】</span></div><ul><li><div><span style="font-size: 16px;">结合里氏替换原则使用</span></div></li></ul><div style="margin-left: 80px;"><span style="font-size: 12pt;">里氏替换原则要求，父类出现的地方子类就能出现，再结合依赖倒置原则，我们可以得出这样一个通俗的规则：接口负责定义public属性和方法，并且声明与其他对象的依赖关系，抽象类负责公共构造部分的实现，实现类准确的实现业务逻辑，同时在适当的时候对父类进行细化。</span></div><ul><li><div><span style="font-size: 12pt;">依赖倒置原则的优点在小型项目中很难体现出来，</span><span style="font-size: 12pt;">是否采用依赖倒置原则影响不大。但是，在一个大中型项目中，采用依赖倒置原则有非常多的优点，特别是规避一些非技术因素引起的问题。</span></div></li><li><div><span style="font-size: 12pt;">项目越大，需求变化的概率也越大，通过采用依赖倒置原则设计的接口或抽象类对实现类进行约束，可以减少需求变化引起的工作量剧增的情况。</span></div></li><li><div><span style="font-size: 12pt;">人员的变动在大中型项目中也是时常存在的，如果设计优良、代码结构清晰，人员变化对项目的影响基本为零。</span></div></li><li><div><span style="font-size: 12pt;">大中型项目的维护周期一般都很长，采用依赖倒置原则可以让维护人员轻松地扩展和维护。</span></div></li><li><div><span style="font-size: 12pt;">依赖倒置原则是6个设计原则中最难以实现的原则，它是实现开闭原则的重要途径，依赖倒置原则没有实现，就别想实现对扩展开放，对修改关闭。</span></div></li><li><div><span style="font-size: 12pt;">在项目中，大家只要记住是“面向接口编程”就基本上抓住了依赖倒置原则的核心。</span></div></li><li><div><span style="font-size: 12pt;">在引入抽象层后，系统将具有很好的灵活性，在程序中尽量使用抽象层进行编程，而将具体类写在配置文件中，这样一来，如果系统行为发生变化，只需要对抽象层进行扩展，并修改配置文件，而无须修改原有系统的源代码，在不修改的情况下来扩展系统的功能，满足开闭原则的要求。</span></div></li><li><div><span style="font-size: 12pt;">在大多数情况下，开闭原则、里氏代换原则、依赖倒转原则这三个设计原则会同时出现，开闭原则是目标，里氏代换原则是基础，依赖倒转原则是手段，它们相辅相成，相互补充，目标一致，只是分析问题时所站角度不同而已。</span></div></li><li><div><span style="font-size: 16px;">然而，</span><span style="font-size: 12pt;">在实际的项目中使用依赖倒置原则时需要审时度势，不要抓住一个原则不放，每一个原则的优点都是有限度的，并不是放之四海而皆准的真理，</span><span style="font-size: 12pt;">作为一个项目经理或架构师，应该懂得技术只是实现目的的工具。不要过分追求依赖倒置原则。</span></div><div><span style="font-size: 12pt;">正如同我们的许多原则一样，应该尽量达到这个原则，而不是随时都要遵循这个原则。我们都很清楚，任何Java程序都有违反这些指导方针的地方！</span></div><div><span style="font-size: 12pt;">但是，如果你深入体验这些方针，将这些方针内化成你思考的一部分，那么在设计时，你将知道何时有足够的理由违反这样的原则比方说，如果有一个不像是会改变的类，那么在代码中直接实例化具体类也就没什么大碍。</span></div><div><span style="font-size: 12pt;">另一方面，如果有个类可能改变，你可以采用一些好技巧（例如工厂方法）来封装改变。</span></div></li><li><div><span style="font-size: 16px;">以下指导方针可以帮助避免在OO设计中违反依赖倒置原则</span></div></li><ul><li><div><span style="font-size: 16px;">变量不可以持有具体类的引用</span></div><div><span style="font-size: 12pt;">如果使用new，就会持有具体类的引用。你可以改用工厂来避开这样的做法。【不直接new一个对象】</span></div></li><li><div><span style="font-size: 12pt;">不要让类派生自具体类</span></div><div><span style="font-size: 12pt;">如果派生自具体类，你就会依赖具体类。请派生自一个抽象（接口或抽象类）。【具体类不做父类】</span></div></li><li><div><span style="font-size: 12pt;">不要覆盖基类中已实现的方法</span></div><div><span style="font-size: 12pt;">如果覆盖基类已实现的方法，那么你的类就不是一个真正适合被继承的抽象。</span><span style="font-size: 12pt; color: unset; font-family: unset;">基类中已实现的方法，应该由所有的子类共享。【虚函数不是覆盖】</span></div></li></ul></ul><div><span style="font-size: 12pt; font-weight: bold;">五、接口隔离原则</span></div><ul><li><div><span style="font-size: 12pt;">定义</span></div></li><li><div><span style="font-size: 12pt;">客户端不应该依赖它不需要的接口。</span></div></li><li><div><span style="font-size: 12pt;">类间的依赖关系应该建立在最小的接口上。</span></div></li><li><div><span style="font-size: 12pt;">使用多个专门的接口，而不使用单一的总接口。</span></div></li><li><div><span style="font-size: 16px;">对定义的理解</span></div></li><li><div><span style="font-size: 12pt;">客户端不应该依赖它不需要的接口</span></div></li><li><div><span style="font-size: 12pt;">依赖它需要的接口，客户端需要什么接口就提供什么接口，把不需要的接口剔除掉</span></div></li><li><div><span style="font-size: 12pt;">需要对接口进行细化，保证其纯洁性；</span></div></li><li><div><span style="font-size: 12pt;">类间的依赖关系应该建立在最小的接口上</span></div></li><li><div><span style="font-size: 12pt;">要求是最小的接口，也是要求接口细化，接口纯洁。</span></div></li><li><div><span style="font-size: 12pt;">建立单一接口，不要建立臃肿庞大的接口。</span></div></li><li><div><span style="font-size: 12pt;">接口尽量细化，同时接口中的方法尽量少。</span></div></li><li><div><span style="font-size: 12pt;">当一个接口太大时，我们需要将它分割成一些更细小的接口，使用该接口的客户端仅需知道与之相关的方法即可。每一个接口应该承担一种相对独立的角色，不干不该干的事，该干的事都要干。</span></div></li><li><div><span style="font-size: 12pt;">接口分为两种：</span></div></li><li><div><span style="font-size: 12pt;">实例接口（Object Interface）</span><span style="font-size: 12pt;">【实际上就是一个具体类】</span></div></li><li><div><span style="font-size: 12pt;">在Java中声明一个类，然后用new关键字产生一个实例，它是对一个类型的事物的描述，这是一种接口。</span></div></li><li><div><span style="font-size: 12pt;">指一个类型所具有的方法特征的集合，仅仅是一种逻辑上的抽象。</span></div></li><li><div><span style="font-size: 12pt;">Java中的类也是一种接口。</span></div></li><li><div><span style="font-size: 12pt;">当把“接口”理解成一个类型所提供的所有方法特征的集合的时候，这就是一种逻辑上的概念，接口的划分将直接带来类型的划分。</span></div></li><li><div><span style="font-size: 12pt;">可以把接口理解成角色，一个接口只能代表一个角色，每个角色都有它特定的一个接口，此时，这个原则可以叫做“角色隔离原则”。</span></div></li><li><div><span style="font-size: 12pt;">类接口（Class Interface）</span></div></li><li><div><span style="font-size: 12pt;">Java中经常使用的interface关键字定义的接口。</span></div></li><li><div><span style="font-size: 12pt;">指某种语言具体的“接口”定义，有严格的定义和结构。</span></div></li><li><div><span style="font-size: 12pt;">接口仅仅提供客户端需要的行为，客户端不需要的行为则隐藏起来</span></div></li><li><div><span style="font-size: 12pt;">应当为客户端提供尽可能小的单独的接口，而不要提供大的总接口。在面向对象编程语言中，实现一个接口就需要实现该接口中定义的所有方法，因此大的总接口使用起来不一定很方便</span></div></li><li><div><span style="font-size: 12pt;">为了使接口的职责单一，需要将大接口中的方法根据其职责不同分别放在不同的小接口中，以确保每个接口使用起来都较为方便，并都承担某一单一角色</span></div></li><li><div><span style="font-size: 16px;">与单一职责原则的区别</span></div></li><li><div><span style="font-size: 12pt;">接口隔离原则与单一职责的审视角度是不相同的</span></div></li><li><div><span style="font-size: 12pt;">单一职责要求的是类和接口职责单一，注重的是职责，这是业务逻辑上的划分。</span></div></li><li><div><span style="font-size: 12pt;">接口隔离原则要求接口的方法尽量少。</span></div></li><li><div><span style="font-size: 12pt;">接口隔离原则要求“尽量使用多个专门的接口”。</span></div></li><li><div><span style="font-size: 12pt;">专门</span><span style="font-size: 12pt;">就是指提供给每个模块的都应该是单一接口，提供给几个模块就应该有几个接口，而不是建立一个庞大的臃肿的接口，容纳所有的客户端访问。</span></div></li><li><div><span style="font-size: 12pt;">接口是我们设计时对外提供的契约，通过分散定义多个接口，可以预防未来变更的扩散，提高系统的灵活性和可维护性。</span></div></li><li><div><span style="font-size: 12pt;">接口隔离原则是对接口进行规范约束，其包含以下4层含义：</span></div></li><li><div><span style="font-size: 12pt;">接口要尽量小</span></div></li><li><div><span style="font-size: 12pt;">这是接口隔离原则的核心定义，不出现臃肿的接口（FatInterface）</span></div></li><li><div><span style="font-size: 12pt;">“小”是有限度的</span></div></li><li><div><span style="font-size: 12pt;">根据接口隔离原则拆分接口时，首先必须满足单一职责原则</span></div></li><li><div><span style="font-size: 12pt;">接口要高内聚</span></div></li><li><div><span style="font-size: 12pt;">高内聚就是提高接口、类、模块的处理能力，减少对外的交互</span></div></li><li><div><span style="font-size: 12pt;">具体到接口隔离原则就是，要求在接口中尽量少公布public方法，接口是对外的承诺，承诺越少对系统的开发越有利，变更的风险也就越少，同时也有利于降低成本。</span></div></li><li><div><span style="font-size: 16px;">定制服务</span></div></li><li><div><span style="font-size: 12pt;">一个系统或系统内的模块之间必然会有耦合，有耦合就要有相互访问的接口</span></div></li><li><div><span style="font-size: 12pt; color: unset; font-family: unset;">在做系统设计时需要考虑对系统之间或模块之间的接口采用定制服务。</span></div></li><li><div><span style="font-size: 12pt;">采用定制服务就必然有一个要求：只提供访问者需要的方法</span></div></li><li><div><span style="font-size: 16px; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; color: rgb(0, 0, 0); font-family: 微软雅黑; font-variant-caps: normal; font-variant-ligatures: normal;">接口应该尽量细化，同时接口中的方法应该尽量少，每个接口中只包含一个客户端（如子模块或业务逻辑类）所需的方法即可，这种机制称为“定制服务”，即为不同的客户端提供宽窄不同的接口。</span></div></li><li><div><span style="font-size: 16px;">接口设计是有限度的</span></div></li><li><div><span style="font-size: 12pt;">接口的设计粒度越小，系统越灵活，这是不争的事实</span></div></li><li><div><span style="font-size: 16px;">但</span><span style="font-size: 12pt;">灵活的同时也带来了结构的复杂化，开发难度增加，可维护性降低</span></div></li><li><div><span style="font-size: 12pt;">接口设计一定要适度，但对于“度”如何判断，根据经验和常识判断，没有一个固化或可测量的标准</span></div></li><li><div><span style="font-size: 12pt;">需要注意控制接口的粒度，接口不能太小，如果太小会导致系统中接口泛滥，不利于维护；接口也不能太大，太大的接口将违背接口隔离原则，灵活性较差，使用起来很不方便。一般而言，接口中仅包含为某一类用户定制的方法即可，不应该强迫客户依赖于那些它们不用的方法。</span></div></li><li><div><span style="font-size: 12pt;">接口隔离原则是对接口的定义，同时也是对类的定义，接口和类尽量使用原子接口或原子类来组装</span></div></li><li><div><span style="font-size: 12pt;">接口隔离原则在实践中可以根据以下几个原则进行：</span></div></li><li><div><span style="font-size: 12pt;">一个接口只服务于一个子模块或业务逻辑</span></div></li><li><div><span style="font-size: 12pt;">通过业务逻辑压缩接口中的public方法，接口时常去回顾，尽量让接口达到精简，而不是肥胖臃肿</span></div></li><li><div><span style="font-size: 12pt;">已经被污染了的接口，尽量去修改，若变更的风险较大，则采用适配器模式进行转化处理</span></div></li><li><div><span style="font-size: 12pt;">了解环境，拒绝盲从。</span><span style="font-size: 12pt;">每个项目或产品都有特定的环境因素，</span><span style="font-size: 12pt;">环境不同，接口拆分的标准就不同。应深入了解业务逻辑，针对性的使用接口隔离原则。</span></div></li><li><div><span style="font-size: 12pt;">接口隔离原则和其他设计原则一样，都需要花费较多的时间和精力来进行设计和筹划，但是它带来了设计的灵活性</span></div></li><li><div><span style="font-size: 12pt;">怎么才能正确地使用接口隔离原则呢？答案是根据经验和常识决定接口的粒度大小，接口粒度太小，导致接口数据剧增，但如果</span><span style="font-size: 12pt; color: unset; font-family: unset;">接口粒度太大，灵活性降低，无法提供定制服务，给整体项目带来无法预料的风险</span></div></li></ul><div><span style="font-size: 12pt; font-weight: bold;">六、迪米特法则</span></div><ul><li><div><span style="font-size: 12pt;">迪米特法则也称为最少知识原则（Least Knowledge Principle，LKP）</span></div></li><li><div><span style="font-size: 16px;">定义</span></div></li><ul><li><div><span style="font-size: 12pt;">一个对象应该对其他对象有最少的了解。</span></div></li><li><div><span style="font-size: 16px;">一个软件实体应当尽可能少的与其他实体发生相互作用。</span></div></li><li><div><span style="font-size: 12pt;">不要和“陌生人”说话。</span></div></li><li><div><span style="font-size: 12pt;">只与你的直接朋友（密友）通信。</span></div></li><li><div><span style="font-size: 16px;">要减少对象之间的交互，只留下几个“密友”。</span></div></li></ul><li><div><span style="font-size: 12pt; color: unset; font-family: unset;">通俗地讲，一个类应该对自己需要耦合或调用的类知道得最少，你（被耦合或调用的类）的内部是如何复杂都和我没关系，那是你的事情，我就知道你提供的这么多public方法，我就调用这么多，其他的我一概不关心。</span></div></li><li><div><span style="font-size: 16px;">迪米特法则告诉我们，当我们正在设计一个系统，不管是任何对象，都要注意它所交互的类有哪些，并注意它和这些类是如何交互的。</span></div><div><span style="font-size: 16px;">这个原则希望我们在设计中，不要让太多的类耦合在一起，免得修改系统中的一部分，会影响到其他部分。</span></div></li><li><div><span style="font-size: 12pt;">如果一个系统符合迪米特法则，那么当其中某一个模块发生修改时，就会尽量少地影响其他模块，扩展会相对容易，这是对软件实体之间通信的限制，迪米特法则要求限制软件实体之间通信的宽度和深度。迪米特法则可降低系统的耦合度，使类与类之间保持松散的耦合关系。</span></div></li><li><div><span style="font-size: 16px;">在迪米特法则中，对于一个对象，其朋友包括以下几类：</span></div></li><ul><li><div><span style="font-size: 12pt;">当前对象本身(this)；</span></div></li><li><div><span style="font-size: 12pt;">以参数形式传入到当前对象方法中的对象；</span></div></li><li><div><span style="font-size: 12pt;">当前对象的成员对象；</span></div></li><li><div><span style="font-size: 12pt;">如果当前对象的成员对象是一个集合，那么集合中的元素也都是朋友；</span></div></li><li><div><span style="font-size: 12pt;">当前对象所创建的对象。</span></div></li></ul></ul><div style="margin-left: 80px;"><span style="font-size: 12pt;">任何一个对象，如果满足上面的条件之一，就是当前对象的“朋友”，否则就是“陌生人”。</span></div><ul><li><div><span style="font-size: 12pt;">在应用迪米特法则时，一个对象只能与直接朋友发生交互，不要与“陌生人”发生直接交互，这样做可以降低系统的耦合度，一个对象的改变不会给太多其他对象带来影响。</span></div></li><li><div><span style="font-size: 12pt;">迪米特法则要求我们在设计系统时，应该尽量减少对象之间的交互，如果两个对象之间不必彼此直接通信，那么这两个对象就不应当发生任何直接的相互作用，如果其中的一个对象需要调用另一个对象的某一个方法的话，可以通过第三者转发这个调用。简言之，就是通过引入一个合理的第三者来降低现有对象之间的耦合度。</span></div></li><li><div><span style="font-size: 12pt; color: unset; font-family: unset;">迪米特法则对类的低耦合提出了明确的要求，其包含以下4层含义：</span></div></li><li><div><span style="font-size: 12pt;">只和朋友交流</span></div></li><li><div><span style="font-size: 16px; color: unset; font-family: unset;">迪米特法则还有一个定义是：</span><span style="font-size: 12pt; color: unset; font-family: unset;">只与直接的朋友通</span><span style="font-size: 12pt; color: unset; font-family: unset;">信。</span></div></li><li><div><span style="font-size: 16px; color: unset; font-family: unset;">直接朋友：</span><span style="font-size: 12pt; color: unset; font-family: unset;">每个对象都必然会与其他对象有耦合关系，两个对象之间的耦合就成为朋友关系，这种关系的类型有很多，例如组合、聚合、依赖等。</span></div></li><li><div><span style="font-size: 12pt;">朋友类的定义是这样的：出现在成员变量、方法的输入输出参数中的类称为成员朋友类，而出现在方法体内部的类不属于朋友类。</span></div></li><li><div><span style="font-size: 12pt;">一个类只和朋友交流，不与陌生类交流，不要出现getA().getB().getC().getD()这种情况（在一种极端的情况下允许出现这种访问，即每一个点号后面的返回类型都相同），类与类之间的关系是建立在类间的，而不是方法间，因此一个方法尽量不引入一个类中不存在的对象，当然，JDK API提供的类除外。</span></div></li><li><div><span style="font-size: 12pt;">朋友间也是有距离的【能不暴露尽量不要暴露】</span></div></li><li><div><span style="font-size: 12pt;">迪米特法则就是对这个距离进行描述，即使是朋友类之间也不能无话不说，无所不知。</span></div></li><li><div><span style="font-size: 12pt;">一个类公开的public属性或方法越多，修改时涉及的面也就越大，变更引起的风险扩散也就越大。因此，为了保持朋友类间的距离，在设计时需要反复衡量：是否还可以再减少public方法和属性，是否可以修改为private、package-private（包类型，在类、方法、变量前不加访问权限，则默认为包类型）、protected等访问权限，是否可以加上final关键字等。</span></div></li><li><div><span style="font-size: 12pt;">迪米特法则要求类“羞涩”一点，尽量不要对外公布太多的public方法和非静态的public变量，尽量内敛，多使用private、package-private、protected等访问权限。</span></div></li><li><div><span style="font-size: 12pt;">是自己的就是自己的</span></div></li><li><div><span style="font-size: 12pt;">如果一个方法放在本类中，既不增加类间关系，也对本类不产生负面影响，那就放置在本类中。</span></div></li><li><div><span style="font-size: 12pt;">谨慎使用Serializable</span></div></li><li><div><span style="font-size: 12pt;">在实际应用中，这个问题是很少出现的，即使出现也会立即被发现并得到解决。</span></div></li><li><div><span style="font-size: 12pt;">迪米特法则的核心观念就是类间解耦，弱耦合，只有弱耦合了以后，类的复用率才可以提高。其要求的结果就是产生了大量的中转或跳转类，导致系统的复杂性提高，同时也为维护带来了难度。</span></div></li><li><div><span style="font-size: 12pt;">在采用迪米特法则时需要反复权衡，既做到让结构清晰，又做到高内聚低耦合。</span></div></li><li><div><span style="font-size: 12pt;">在实际应用中，如果一个类跳转两次以上才能访问到另一个类，就需要想办法进行重构了。</span></div></li><li><div><span style="font-size: 12pt;">一个系统的成功不仅仅是一个标准或是原则就能够决定的，有非常多的外在因素决定，跳转次数越多，系统越复杂，维护就越困难，所以只要跳转不超过两次都是可以忍受的，这需要具体问题具体分析。</span></div></li><li><div><span style="font-size: 12pt;">迪米特法则要求类间解耦，但解耦是有限度的，</span><span style="font-size: 12pt;">在实际的项目中，需要适度地考虑这个原则，别为了套用原则而做项目。原则只是供参考，如果违背了这个原则，项目也未必会失败，这就需要在采用原则时反复度量，不遵循是不对的，严格执行就是“过犹不及”。</span></div></li><li><div><span style="font-size: 12pt;">在将迪米特法则运用到系统设计中时，要注意下面的几点：</span></div></li><li><div><span style="font-size: 12pt;">在类的划分上，应当尽量创建松耦合的类，类之间的耦合度越低，就越有利于复用，一个处在松耦合中的类一旦被修改，不会对关联的类造成太大波及；</span></div></li><li><div><span style="font-size: 12pt;">在类的结构设计上，每一个类都应当尽量降低其成员变量和成员函数的访问权限；</span></div></li><li><div><span style="font-size: 12pt;">在类的设计上，只要有可能，一个类型应当设计成不变类；</span></div></li><li><div><span style="font-size: 12pt;">在对其他类的引用上，一个对象对其他对象的引用应当降到最低。</span></div></li><li><div><span style="font-size: 16px;">这个原则减少了对象之间的依赖，研究显示这会减少软件的维护成本，但是采用这个原则也会导致更多的“包装”类被制造出来，以处理和其他组件的沟通，这可能会导致复杂度和开发时间的增加，并降低运行时的性能。</span></div></li></ul><div><span style="font-size: 12pt; font-weight: bold;">七、合成复用原则</span></div><ul><li><div><span style="font-size: 16px;">合成复用原则（Composite Reuse Principle，CRP）</span><span style="font-size: 12pt;">又称为组合/聚合复用原则(Composition/Aggregate Reuse Principle, CARP)</span></div></li><li><div><span style="font-size: 16px;">定义</span></div></li><li><div><span style="font-size: 12pt;">尽量使用对象组合，而不是继承来达到复用的目的</span></div></li><li><div><span style="font-size: 12pt;">复用时要尽量使用组合/聚合关系（关联关系），少用继承</span></div><div><span style="font-size: 12pt; color: unset; font-family: unset;">合成复用原则就是在一个新的对象里通过关联关系（包括组合关系和聚合关系）来使用一些已有的对象，使之成为新对象的一部分；新对象通过委派调用已有对象的方法达到复用功能的目的。</span></div></li><li><div><span style="font-size: 16px;">尽管继承为例强大，但是它便能够不总是能够实现最有弹性和最好维护的设计。利用组合和委托可以在运行时具有继承行为的效果。</span></div><div><span style="font-size: 16px;">利用继承设计子类的行为，是在编译时静态决定的，而且所有的子类都会继承到相同的行为。然而，如果能够利用组合的做法扩展对象的行为，就可以在运行时动态的进行扩展。</span></div></li><li><div><span style="font-size: 16px;">通过动态的组合对象，可以写新的代码添加新的功能，而无须修改现有代码。既然没有改变现有代码，那么引进Bug或产生意外副作用的机会将大幅减少。</span></div></li><li><div><span style="font-size: 12pt;">在面向对象设计中，可以通过两种方法在不同的环境中复用已有的设计和实现</span></div></li><li><div><span style="font-size: 12pt;">通过组合/聚合关系</span></div></li><li><div><span style="font-size: 12pt;">首先应该考虑使用组合/聚合，组合/聚合可以使系统更加灵活，降低类与类之间的耦合度，一个类的变化对其他类造成的影响相对较少；</span></div></li><li><div><span style="font-size: 12pt;">由于组合或聚合关系可以将已有的对象（也可称为成员对象）纳入到新对象中，使之成为新对象的一部分，因此新对象可以调用已有对象的功能，这样做可以使得成员对象的内部实现细节对于新对象不可见，所以这种复用又称为“黑箱”复用。</span></div></li><li><div><span style="font-size: 12pt;">相对继承关系而言，</span><span style="font-size: 12pt;">合成复用</span><span style="font-size: 12pt;">耦合度相对较低，成员对象的变化对新对象的影响不大，可以在新对象中根据实际需要有选择性地调用成员对象的操作。</span></div></li><li><div><span style="font-size: 12pt;">合成复用可以在运行时动态进行，新对象可以动态地引用与成员对象类型相同的其他对象。</span></div></li><li><div><span style="font-size: 12pt;">通过继承</span></div></li><li><div><span style="font-size: 12pt;">其次才考虑继承，在使用继承时，需要严格遵循里氏代换原则，有效使用继承会有助于对问题的理解，降低复杂度，而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度，因此需要慎重使用继承复用。</span></div></li><li><div><span style="font-size: 12pt;">通过继承来进行复用的主要问题在于继承复用会破坏系统的封装性，因为继承会将基类的实现细节暴露给子类，由于基类的内部细节通常对子类来说是可见的，</span><span style="font-size: 12pt; color: unset; font-family: unset;">这种复用又称“白箱”复用</span></div></li><li><div><span style="font-size: 12pt; color: unset; font-family: unset;">如果基类发生改变，那么子类的实现也不得不发生改变；</span></div></li><li><div><span style="font-size: 12pt;">从基类继承而来的实现是静态的，不可能在运行时发生改变，没有足够的灵活性。</span></div></li><li><div><span style="font-size: 12pt;">而且继承只能在有限的环境中使用（如类没有声明为不能被继承）。</span></div></li><li><div><span style="font-size: 12pt; color: unset; font-family: unset;">一般而言，如果两个类之间是“Has-A”的关系应使用组合或聚合，如果是“Is-A”关系可使用继承。</span></div></li><li><div><span style="font-size: 12pt; color: unset; font-family: unset;">&quot;Is-A&quot;是严格的分类学意义上的定义，意思是一个类是另一个类的&quot;一种&quot;；</span></div></li><li><div><span style="font-size: 12pt; color: unset; font-family: unset;">&quot;Has-A&quot;则不同，它表示某一个角色具有某一项责任。</span></div></li></ul><div><span style="font-size: 12pt; font-weight: bold;">八、松耦合设计原则</span></div><ul><li><div><span style="font-size: 16px;">为了交互对象之间的松耦合设计而努力</span></div></li><li><div><span style="font-size: 16px;">松耦合的设计之所以能让我们建立有弹性的OO系统，能够应对变化，是因为对象之间的互相依赖降到了最低。</span></div></li><li><div><span style="font-size: 16px;">当两个对象之间松耦合，它们依然可以交互，但是不太清楚彼此的细节</span></div></li></ul><div><font style="font-size: 12pt;"><span style="font-size: 12pt; font-weight: bold;">九、好莱坞原则</span></font></div><ul><li><div><span style="font-size: 16px;">定义</span></div></li><ul><li><div><span style="font-size: 16px;">别调用（打电话）我们，我们会调用（打电话给）你。</span></div></li></ul><li><div><span style="font-size: 16px;">好莱坞原则提供一种防止“依赖腐败”的方法。</span></div><div><span style="font-size: 16px;">当高层组件依赖低层组件，而低层组件又依赖高层组件，而高层组件又依赖边侧组件，而边侧组件又依赖低层组件时，依赖腐败就发生了。在这种情况下，没有人可以轻易的搞懂系统使如何设计的。</span></div></li><li><div><span style="font-size: 16px;">在好莱坞原则下，我们允许低层组件将自己挂钩到系统上，但是高层组件会决定什么时候和怎样使用这些低层组件。换句话说，高层组件对待低层组件的方式是“别调用我们，我们会调用你”。</span></div></li><li><div><span style="font-size: 16px;">好莱坞原则用在创建框架或者组件上的一种技巧，好让低层组件能够被挂钩进计算中，而且又不会让高层组件依赖低层组件</span></div></li><li><div><span style="font-size: 16px;">好莱坞原则的目标在于解耦</span></div></li><li><div><span style="font-size: 16px;">好莱坞原则教给我们一个技巧，创建一个有弹性的设计，允许低层结构能够互相操作，而又防止其他类太过依赖它们。</span></div></li><li><div><span style="font-size: 16px;">低层组件不是一定不可以调用高层组件中的方法。事实上，低层组件在结束时，常常会调用从超类中继承来的方法。我们需要避免让高层和低层组件之间有明显的环状依赖。</span></div></li><li><div><span style="font-size: 16px;">好莱坞原则告诉我们，将决策权放在高层模块中，以便决定如何以及何时调用低层模块。</span></div></li></ul><div><br clear="none"/></div><div><br clear="none"/></div><div><br clear="none"/></div><div><br clear="none"/></div><div><br clear="none"/></div><div><br clear="none"/></div><div><br clear="none"/></div><div><br clear="none"/></div><div><span style="font-size: 12pt; font-weight: bold;">十、其他相关讨论</span></div><ul><li><div><span style="font-size: 16px; font-style: italic;">对于单元测试的讨论：</span></div></li><li><div><span style="font-size: 12pt; font-style: italic;">在单元测试中，有一句非常有名的话，叫做&quot;Keep the bar green to keep the code clean&quot;，即保持绿条有利于代码整洁，这是什么意思呢？绿条就是Junit运行的两种结果中的一种：要么是红条，单元测试失败；要么是绿条，单元测试通过。一个方法的测试方法一般不少于3种，为什么呢？首先是正常的业务逻辑要保证测试到，其次是边界条件要测试到，然后是异常要测试到，比较重要的方法的测试方法甚至有十多种。</span></div></li><li><div><span style="font-size: 12pt; font-style: italic;">在实际的项目中，一个类一般只有一个测试类，其中可以有很多的测试方法。</span></div></li><li><div><span style="font-size: 16px; font-style: italic;">单元测试是孤立测试。</span></div></li><li><div><span style="font-size: 12pt; font-style: italic;">Web Service开发就应该知道有一个“契约优先”的原则，也就是先定义出WSDL接口，制定好双方的开发协议，然后再各自实现。</span></div></li></ul><div><br clear="none"/></div><div><br clear="none"/></div><div><br clear="none"/></div><div><br clear="none"/></div><div><br clear="none"/></div><div><br clear="none"/></div><div><br clear="none"/></div><div><br clear="none"/></div><div><br clear="none"/></div><div><br clear="none"/></div></div><div><br clear="none"/></div></span>
</div></body></html> 